/*
This file is part of Socks via HTTP.

This package is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

Socks via HTTP is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Socks via HTTP; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

// Title :        ThreadCommunication.java
// Version :      1.0
// Copyright :    Copyright (c) 2001-2002
// Author :       Florent CUETO  & Sebastien LEBRETON <socksviahttp@cqs.dyndns.org>
// Description :  Communication between socks via HTTP client & the servlet (HTTP Tunneling)

package socksviahttp.client.engine;

import java.net.*;
import java.io.*;
import java.util.zip.*;

import socksviahttp.core.consts.*;
import socksviahttp.core.log.*;
import socksviahttp.core.net.*;
import socksviahttp.core.util.*;
import socksviahttp.client.net.*;

public class ThreadCommunication extends Thread
{
  private Connection source = null;
  private String id_conn = null;
  private String destinationUri = null;
  private boolean initOk = false;
  private Configuration configuration = null;
  private boolean requestOnlyIfClientActivity = false;

  public ThreadCommunication(Connection source, Configuration configuration)
  {
    super();
    this.source = source;
    this.configuration = configuration;
    //this.requestOnlyIfClientActivity = configuration.isRequestOnlyIfClientActivity();
    this.requestOnlyIfClientActivity = false;

    // Get the destination
    GenericSocksHandler socksHandler = null;
    try
    {
      socksHandler = GenericSocksHandler.getHandler(this.configuration, this.source);
    }
    catch(Exception e)
    {
      configuration.printlnWarn("<CLIENT> No valid Socks handler found");
      return;
    }

    destinationUri = (socksHandler.getDnsName() != null ? socksHandler.getDnsName() : socksHandler.getDestIP()) + ":" + socksHandler.getDestPort();
    configuration.printlnInfo("<CLIENT> An application asked a connection to " + destinationUri);
    configuration.printlnDebug("<CLIENT> Handler type : " + socksHandler.getLabel());

    // Create a connection request
    DataPacket dataPacket = new DataPacket();
    dataPacket.type = Const.CONNECTION_CREATE;
    dataPacket.id = configuration.getUser() + ":" + configuration.getPassword() + ":" + configuration.getTimeout();
    dataPacket.tab = destinationUri.getBytes();

    // Send the connection request to the HTTP server
    int type = Const.CONNECTION_UNSPECIFIED_TYPE;
    String serverInfoMessage = null;
    try
    {
      configuration.printlnInfo("<CLIENT> SERVER, create a connection to " + destinationUri);
      DataPacket response = sendHttpMessage(configuration, dataPacket);
      type = response.type;
      id_conn = response.id;
      serverInfoMessage = new String(response.tab);
    }
    catch(Exception e)
    {
      configuration.printlnError("<CLIENT> Cannot initiate a dialog with SERVER. Exception : " + e);
      return;
    }

    if (type == Const.CONNECTION_CREATE_OK)
    {
      initOk = true;
      configuration.printlnInfo("<SERVER> Connection created : " + id_conn);

      // Send the response packet to the socks client
      GenericSocksHandler replyPacket = socksHandler;
      String[] resp = StringUtils.stringSplit(serverInfoMessage, ":", false);
      replyPacket.setDestIP(resp[0]);
      replyPacket.setDestPort(Integer.parseInt(resp[1]));
      this.source.write(replyPacket.buildResponse(GenericSocksHandler.RESPONSE_SUCCESS));
    }
    else
    {
      configuration.printlnError("<SERVER> " + serverInfoMessage);

      // Send the response packet to the socks client
      GenericSocksHandler replyPacket = socksHandler;
      this.source.write(replyPacket.buildResponse(GenericSocksHandler.RESPONSE_FAILURE));
    }
  }

  public ThreadCommunication(Connection source, String destinationUri, Configuration configuration)
  {
    super();
    this.source = source;
    this.destinationUri = destinationUri;
    this.configuration = configuration;

    configuration.printlnInfo("<CLIENT> An application asked a connection to " + destinationUri);

    // Create a connection on the servlet server
    DataPacket dataPacket = new DataPacket();
    dataPacket.type = Const.CONNECTION_CREATE;
    dataPacket.id = configuration.getUser() + ":" + configuration.getPassword() + ":" + configuration.getTimeout();
    dataPacket.tab = destinationUri.getBytes();

    // Send the connection
    int type = Const.CONNECTION_UNSPECIFIED_TYPE;
    String serverInfoMessage = null;
    try
    {
      configuration.printlnInfo("<CLIENT> SERVER, create a connection to " + destinationUri);
      DataPacket response = sendHttpMessage(configuration, dataPacket);
      type = response.type;
      id_conn = response.id;
      serverInfoMessage = new String(response.tab);
    }
    catch(Exception e)
    {
      configuration.printlnError("<CLIENT> Cannot initiate a dialog with SERVER. Exception : " + e);
      return;
    }

    if (type == Const.CONNECTION_CREATE_OK)
    {
      initOk = true;
      configuration.printlnInfo("<SERVER> Connection created : " + id_conn);
    }
    else
    {
      configuration.printlnError("<SERVER> " + serverInfoMessage);
    }
  }

  // Main task
  public void run()
  {
    if (!initOk)
    {
      configuration.printlnError("<CLIENT> Disconnecting application");
      source.disconnect();
      return;
    }

    boolean dialogInProgress = true;
    byte[] line;
    long initialTime = new java.util.Date().getTime();
    long lastUpdateTime = initialTime;

    while (dialogInProgress == true)
    {
      try
      {
        line = source.read();

        long now = new java.util.Date().getTime();

        // Check if we have to start minimizing HTTP traffic
        if (now - initialTime > configuration.getDontTryToMinimizeTrafficBefore()) requestOnlyIfClientActivity = configuration.isRequestOnlyIfClientActivity();

        boolean forceRequest = (now > configuration.getForceRequestAfter() + lastUpdateTime);
        if (configuration.getForceRequestAfter() == 0) forceRequest = false;
        if ((!requestOnlyIfClientActivity) || (forceRequest) || (line == null) || (line.length > 0))
        {
          lastUpdateTime = new java.util.Date().getTime();
          DataPacket dataPacket = new DataPacket();
          dataPacket.id = id_conn;

          if (line == null)
          {
            // Connection closed
            configuration.printlnInfo("<CLIENT> Application closed the connection");
            configuration.printlnInfo("<CLIENT> SERVER, close the connection " + id_conn);
            requestOnlyIfClientActivity = false; // Speeds up the shutdown
            dataPacket.type = Const.CONNECTION_DESTROY;
            dataPacket.tab = Const.TAB_EMPTY;
          }
          else
          {
            //configuration.printlnDebug("<CLIENT> SERVER, update the connection " + id_conn);
            dataPacket.type = Const.CONNECTION_REQUEST;
            dataPacket.tab = line;

            if ((configuration.getSpyMode() == Configuration.SPY_MODE_CLIENT) || (configuration.getSpyMode() == Configuration.SPY_MODE_BOTH))
            {
              // Log server data to file
              if (line.length > 0) new DiskAccess(id_conn + "_client.log", line).start();
            }
          }

          // Send the message
          boolean packetTransmitted = false;
          int retry = 0;
          DataPacket response = null;
          while((!packetTransmitted) && (retry < 1 + configuration.getMaxRetries()))
          {
            try
            {
              response = sendHttpMessage(configuration, dataPacket);
              packetTransmitted = true;
            }
            catch(Exception e)
            {
              retry++;
              configuration.printlnWarn("<CLIENT> Cannot reach SERVER (try #" + retry + "). Exception : " + e);
              Thread.sleep(configuration.getDelayBetweenTries());
            }
          }
          if (retry == 1 + configuration.getMaxRetries())
          {
            configuration.printlnError("<CLIENT> The maximum number of retries has been done");
            configuration.printlnError("<CLIENT> Disconnecting application");
            source.disconnect();
            dialogInProgress = false;
            return;
          }

          // Write the received bytes
          switch (response.type)
          {
            case Const.CONNECTION_RESPONSE:
              if ((configuration.getSpyMode() == Configuration.SPY_MODE_SERVER) || (configuration.getSpyMode() == Configuration.SPY_MODE_BOTH))
              {
                // Log server data to file
                if (response.tab.length > 0) new DiskAccess(id_conn + "_client.log", response.tab).start();
              }
              source.write(response.tab);
              break;
            case Const.CONNECTION_NOT_FOUND:
              configuration.printlnError("<SERVER> Connection not found : " + id_conn);
              break;
            case Const.CONNECTION_DESTROY_OK:
              configuration.printlnInfo("<SERVER> As CLIENT asked, connection closed : " + id_conn);
              break;
            default:
              configuration.printlnWarn("<CLIENT> SERVER sent an unexpected response type : " + response.type);
              break;
          }

          // If the connection has been closed
          if (response.isConnClosed)
          {
            // Log
            configuration.printlnInfo("<SERVER> Remote server closed the connection : " + response.id);

            // Close the source connection
            configuration.printlnInfo("<CLIENT> Disconnecting application");
            source.disconnect();

            // Stop the thread
            dialogInProgress = false;
          }

          if (response.type == Const.CONNECTION_DESTROY_OK)
          {
            // Close the source connection
            configuration.printlnInfo("<CLIENT> Disconnecting application");
            source.disconnect();

            // Stop the thread
            dialogInProgress = false;
          }

          if (response.type == Const.CONNECTION_NOT_FOUND)
          {
            // Close the source connection
            configuration.printlnError("<CLIENT> Disconnecting application");
            source.disconnect();

            // Stop the thread
            dialogInProgress = false;
          }
        }

        // Sleep
        //configuration.printlnDebug("<CLIENT> Sleeping " + configuration.getDelay() + " ms");
        Thread.sleep(configuration.getDelay());
      }
      catch (Exception e)
      {
        configuration.printlnError("<CLIENT> Unexpected Exception : " + e);
      }
    }
  }

  public static DataPacket sendHttpMessage(Configuration config, DataPacket source) throws IOException, ClassNotFoundException
  {
    // Send an HTTP message
    DataPacket ret = null;
    InputStream is = null;
    ObjectInputStream ois = null;

    HttpMessage mess = new HttpMessage(config.getUrl());
    if (config.isProxyNeedsAuthentication())
    {
      // Set the proxy authorization
      mess.setProxyAuthorization(config.getProxyUser(), config.getProxyPassword());
    }

    // Create the InputStream
    is = mess.sendGZippedPostMessage(source);

    // Create the GZIPInputStream
    GZIPInputStream zis = new GZIPInputStream(is);

    // Create the ObjectInputStream
    ois = new ObjectInputStream(zis);

    // Read the response
    ret = (DataPacket)ois.readObject();

    // Close the stream
    ois.close();

    // Return the value
    return(ret);
  }
}
